Mapper Container topic

MapperContainers are the backbone of dart_mappable. A MapperContainers job is to lookup the correct mapper for a type and call its respective method.

To find the mapper for a given type, the container first looks at its own set of mappers and when there is no match it refers to its linked containers.

Working with MapperContainers

This is an advanced topic. Usually you don't need to worry about containers, as they are hidden from the user when doing standard things like decoding or encoding from json or using the generated mixins toString, == or hashCode overrides.

You don't need to know this for most use-cases. But feel free to read on if you are interested.

However each generated mapper internally defines and uses its own MapperContainer, which you can access using the <ClassName>Mapper.container getter.

When you look at the interface of a mapper, you can see all the respective methods for serialization and mapping, like fromJson, toJson, isEqual, hash and asString. You can use these methods with any object as long as the container can resolve a mapper for it.

To control which types a MapperContainer can resolve see the following methods:

  • use<T>(MapperBase<T> mapper) will add a new mapper for type T.

  • unuse<T>() will remove the current mapper for type T. Note that this works for any mapper, custom, generated or for primitive types.

  • useAll(Iterable<MapperBase> mapper) works as use() but for a list of mappers.

  • get<T>() will return the current mapper for type T.

  • getAll() will return all currently registered mappers.

Additionally to controlling mappers manually for a single container, you can also link a container to another. That way container A can use all mappers of container B implicitly.

  • link(MapperContainer container) links the provided container to the current one.

The following example shows this in action for a schematic setup of two classes and mappers.


// Suppose we have two classes and their respective mappers
class A {}
class B {}

class AMapper extends MapperBase<A> {} // generated
class BMapper extends MapperBase<B> {} // generated

void main() {
  
  // succeeds - generated mappers have their own container
  AMapper.container.toJson(A());
  
  // fails - generated mappers only work with their respective types
  BMapper.container.toJson(A());
  
  // creates a new empty container
  var containerX = MapperContainer();

  // fails - no mappers were added yet
  containerX.toJson(A());

  // succeeds - added the respective mapper
  containerX.use(AMapper());
  containerX.toJson(A());
  
  var containerY = MapperContainer();
  containerY.use(BMapper());

  // succeeds - added the respective mapper
  containerY.toJson(B());
  // fails - missing mapper for type A
  containerY.toJson(A());
  
  // links containerX to containerY
  containerY.link(containerX);

  // succeeds - mapper is resolved through linked containerX
  containerY.toJson(A());
  
  // fails - linking is one-directional
  containerX.toJson(B());
}

Default Container

There is a special global default container that can be accessed using MapperContainer.defaults. This container initially contains mappers for all primitive and core types. Types included are:

dynamic, Object, String, int, double, num, bool, DateTime, List, Set, Map

All other containers are implicitly linked to this default container.

You can use this container to add mappers for types, that are thereby supported by all other containers.

// suppose we have a class and a custom mapper for it
class MyClass {}
class MyClassMapper extends SimpleMapper<MyClass> { ... }

void main() {
  
  var container = MapperContainer();
  // this doen't work, since the mapper for type [MyClass] wasn't added yet to this container
  container.toJson(MyClass());
  
  // we add the mapper as a global default
  MapperContainer.defaults.use(MyClassMapper());

  // now this works, since all containers are linked to the default container
  container.toJson(MyClass());
}

Combined Containers

For convenience or when using generic decoding you may want to have a container that knows all classes in your project. Such a container can be auto-generated using the @MappableLib(createCombinedContainer: true) annotation.

This feature is documented here.

Encoding Lists, Sets and Maps

Because all of the MapperContainers methods are generic, you are not limited to use a single object with these methods. dart_mappable also support Lists, Sets and Maps out of the box, without any special syntax, workarounds or hacks.

@MappableClass()
class Dog with DogMappable {
  String name;
  Dog(this.name);
}

@MappableClass()
class Box<T> with BoxMappable<T> {
  T content;
  Box(this.content);
}

void main() {
  // Case 1: Simple list.
  // We use the default container since we only use core types.
  List<int> nums = MapperContainer.defaults.fromJson('[2, 4, 105]');
  print(nums); // [2, 4, 105]

  // Case 2: Set of objects.
  // We use the generated container for [Dog], but with a set of objects.
  Set<Dog> dogs = DogMapper.container.fromJson('[{"name": "Thor"}, {"name": "Lasse"}, {"name": "Thor"}]');
  print(dogs); // {Dog(name: Thor), Dog(name: Lasse)}

  // Case 3: More complex lists, like generics.
  // We use the generated container for [Box], but with a list of generic objects.
  List<Box<double>> boxes = BoxMapper.container.fromJson('[{"content": 0.1}, {"content": 12.34}]');
  print(boxes); // [Box(content: 0.1), Box(content: 12.34)]
}

There is also the MapperContainer.fromIterable method. This can be used if you already have a list of dynamic objects instead of the raw json string. Additionally this can get handy to decode a dynamic list of partly-encoded values:

List<double> myNumbers = MapperContainer.defaults.fromIterable([2.312, '1.32', 500, '1e4']);
print(myNumbers); // [2.312, 1.32, 500.0, 10000.0]

Non-Trivial Maps

We also support decoding of non-trivial maps. Although the use-cases might be rare, you can decode to something other than a map of string keys like this:

var encodedMap = {
  {'name': 'Bonny'}: 1,
  {'name': 'Clyde'}: 5,
};

Map<Dog, int> treatsPerDog = DogMapper.container.fromValue(encodedMap);
print(treatsPerDog[Dog('Clyde')]!); // 5

var myMap = DogMapper.container.toValue(treatsPerDog);
print(myMap); // {{name: Bonny}: 1, {name: Clyde}: 5}

Note: Make sure to add @MappableClass on your key class, in order to enable easy property access.

Note: Since json only supports string keys, we can't do fromJson or toJson on these maps. You would have to decode / encode your keys and values separately.


🎉 Congrats, you followed the tour until the end. Now you know everything about this package.

Exceptions / Errors

MapperException Mapper Container
General exception class used throughout the package